package lumis.content.core;

import lumis.content.table.ContentQueryBuilder;
import lumis.doui.DouiManagerFactory;
import lumis.doui.dao.DouiDaoFactory;
import lumis.doui.search.common.DouiIndexConfig;
import lumis.doui.search.common.DouiIndexerFactory;
import lumis.doui.search.common.IDouiIndexer;
import lumis.doui.search.common.ParametersSourceDataWrapper;
import lumis.doui.source.ISourceData;
import lumis.doui.source.SourceContext;
import lumis.doui.source.SourceFactory;
import lumis.doui.source.TabularData;
import lumis.doui.table.QueryBuilder;
import lumis.doui.table.TableSource;
import lumis.portal.PortalException;
import lumis.portal.UnexpectedException;
import lumis.portal.authentication.SessionConfig;
import lumis.portal.clock.ClockConfig;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.service.GenericServiceClock;
import lumis.portal.serviceinstance.ServiceInstanceConfig;
import lumis.portal.transaction.PortalTransactionFactory;
import lumis.util.CollectionExecutor;
import lumis.util.ITransaction;
import lumis.util.XmlUtil;
import lumis.util.parameter.Parameters;
import lumis.util.query.IQueryFilter;
import lumis.util.query.QueryDelete;
import lumis.util.query.QueryField;
import lumis.util.query.QueryFilter;
import lumis.util.query.QuerySelect;
import lumis.util.query.QueryValue;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.w3c.dom.Node;

/**
 * Responsible for cleaning old {@link ContentVersion versions} of
 * {@link ContentLocale#setDirty(boolean) dirty} {@link ContentLocale content
 * locales}.
 * 
 * @version $Revision: 13092 $ $Date: 2011-05-28 18:19:06 -0300 (Sat, 28 May 2011) $
 * @since 6.0.0
 */
public class ContentVersionCleanerClock extends GenericServiceClock
{
	// Constants
	private static final String MAX_VERSIONS_PARAM = "maxVersions";
	private static final String SELECT_VERSIONS_FOR_DELETION = "select cv from ContentVersion cv inner join cv.contentLocale l where l = :locale "
			+ "and cv.versionNumber <= l.activeVersion.versionNumber - :maxVersions "
			+ "and cv != l.activeVersion and (l.publishedVersion is null or (l.publishedVersion is not null and cv != l.publishedVersion)) "
			+ "order by cv.versionNumber";
	private static final String LOCALE_PARAM = "locale";
	private static final long TIME_FOR_STOPPING = 1800000L;
	private static final int MAX_CONTENT_LOCALE_BLOCK = 10;
	private static final int MAX_CONTENT_VERSIONS_BLOCK = 1;
	private static final String DIRTY_PARAM = "dirty";
	private static final String SELECT_DIRTY_LANGUAGES = "select cl.id from ContentLocale cl where cl.dirty = :dirty";

	private Map<String, Node> contentSourcesVersioningNodes = new HashMap<String, Node>();
	private final long startTime = System.currentTimeMillis();
	private long maxEndTime;

	@SuppressWarnings("unchecked")
	public void doTick(SessionConfig sessionConfig, ClockConfig clockConfig) throws PortalException
	{
		maxEndTime = startTime + clockConfig.getMaxRunTime() * 1000;

		try
		{
			Collection<String> dirtyLocales = null;
			ITransaction transaction = PortalTransactionFactory.createTransaction();
			try
			{
				transaction.begin();
				dirtyLocales = ManagerFactory.getEntityManager().createQuery(SELECT_DIRTY_LANGUAGES).setParameter(
						DIRTY_PARAM, Boolean.TRUE).setMaxResults(MAX_CONTENT_LOCALE_BLOCK).getResultList();
			}
			finally
			{
				transaction.dispose();
			}

			// while the read block is full (read size == max) process it and
			// read the next block
			while (dirtyLocales != null && dirtyLocales.size() == MAX_CONTENT_LOCALE_BLOCK && checkRunning())
			{
				processContentLocales(sessionConfig, dirtyLocales);
				transaction = PortalTransactionFactory.createTransaction();
				try
				{
					transaction.begin();
					dirtyLocales = ManagerFactory.getEntityManager().createQuery(SELECT_DIRTY_LANGUAGES).setParameter(
							DIRTY_PARAM, Boolean.TRUE).setMaxResults(MAX_CONTENT_LOCALE_BLOCK).getResultList();
				}
				finally
				{
					transaction.dispose();
				}
			}

			// process the remaining block
			processContentLocales(sessionConfig, dirtyLocales);
		}
		catch (InterruptedException e)
		{
			// will gently terminate the tick execution
		}
		finally
		{
			// since contentSourcesVersioningNodes will not be cleared if source definition is changed,
			// just clearing it after each clock tick
			contentSourcesVersioningNodes.clear();
		}
	}

	/**
	 * Cleans a {@link Collection} of dirty {@link ContentLocale locales}.
	 * 
	 * @param sessionConfig
	 *            the session config to use.
	 * @param dirtyLocales
	 *            the dirty locales
	 * @throws InterruptedException
	 * @since 6.0.0
	 */
	private void processContentLocales(SessionConfig sessionConfig, Collection<String> dirtyLocales) throws PortalException,
			InterruptedException
	{
		if (dirtyLocales != null && !dirtyLocales.isEmpty())
		{
			for (String dirtyLocale : dirtyLocales)
			{
				if (!checkRunning())
					return;
				cleanUpContentLocale(sessionConfig, dirtyLocale);
			}
		}
	}

	/**
	 * Clean up content locale's versions.
	 * 
	 * @param sessionConfig
	 *            the session config to use.
	 * @param contentLocaleId
	 *            the content locale identifier to be cleaned up.
	 * @throws InterruptedException
	 * @since 6.0.0
	 */
	private void cleanUpContentLocale(SessionConfig sessionConfig, String contentLocaleId) throws PortalException, InterruptedException
	{
		ContentLocale contentLocale;
		Node sourceNode;
		String sourceNodeKey;
		int maxVersions;
		ServiceInstanceConfig serviceInstanceConfig;
		ITransaction transaction = PortalTransactionFactory.createTransaction();
		try
		{
			transaction.begin();

			contentLocale = ManagerFactory.getEntityManager().find(ContentLocale.class, contentLocaleId);
			String serviceId = ManagerFactory.getServiceInstanceManager().get(contentLocale.getContent().getServiceInstanceId(),
					transaction).getServiceId();

			sourceNodeKey = serviceId + "#" + contentLocale.getContent().getSourceId();
			sourceNode = contentSourcesVersioningNodes.get(sourceNodeKey);
			if (sourceNode == null)
			{
				sourceNode = XmlUtil.selectSingleNode("/douiDefinition/service/sources/source[@id='"
						+ contentLocale.getContent().getSourceId()
						+ "']", DouiManagerFactory.getDouiManager().getDouiServiceDefinitionNode(serviceId, transaction));

				contentSourcesVersioningNodes.put(sourceNodeKey, sourceNode);
			}

			String maxVersionsStr = XmlUtil.readAttributeString(MAX_VERSIONS_PARAM, XmlUtil.selectSingleNode("versioning", sourceNode));
			if (maxVersionsStr == null || "".equals(maxVersionsStr))
			{
				contentLocale.setDirty(false);
				transaction.commit();
				return;
			}

			maxVersions = Integer.parseInt(maxVersionsStr);
			if (maxVersions < 0)
			{
				contentLocale.setDirty(false);
				transaction.commit();
				return;
			}

			String serviceInstanceId = contentLocale.getContent().getServiceInstanceId();
			serviceInstanceConfig = ManagerFactory.getServiceInstanceManager().get(serviceInstanceId, transaction);
			transaction.commit();
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
		finally
		{
			transaction.dispose();
		}
		removeVersions(sessionConfig, contentLocale, sourceNode, maxVersions, serviceInstanceConfig);
	}

	/**
	 * Removes the {@link ContentVersion versions} of a {@link ContentLocale
	 * locale}.
	 * 
	 * @param sessionConfig
	 *            the session config to use.
	 * @param contentLocale
	 *            the content locale.
	 * @param sourceNode
	 *            the content locale's source node.
	 * @param maxVersions
	 *            the maximum number of versions to keep.
	 * @param config
	 *            the service instance config related to the content locale.
	 * @throws InterruptedException
	 * @since 6.0.0
	 */
	private void removeVersions(SessionConfig sessionConfig, ContentLocale contentLocale, Node sourceNode,
			int maxVersions, ServiceInstanceConfig config) throws PortalException, InterruptedException
	{
		Collection<ContentVersion> toBeRemoved = null;
		ITransaction transaction = PortalTransactionFactory.createTransaction();
		try
		{
			transaction.begin();

			// prevents content reindexing
			transaction.setAttribute(ContentIndexerTransactionObserver.BYPASS_ATTRIBUTE_NAME, Boolean.TRUE);
			while (checkRunning() && (toBeRemoved = selectVersions(contentLocale, maxVersions)).size() == MAX_CONTENT_VERSIONS_BLOCK)
			{
				removeVersionAndData(sessionConfig, sourceNode, config, toBeRemoved, transaction);
				transaction.commit();
				transaction.dispose();
				transaction = PortalTransactionFactory.createTransaction();
				transaction.begin();

				// prevents content reindexing
				transaction.setAttribute(ContentIndexerTransactionObserver.BYPASS_ATTRIBUTE_NAME, Boolean.TRUE);
			}
			if (checkRunning())
			{
				removeVersionAndData(sessionConfig, sourceNode, config, toBeRemoved, transaction);
				contentLocale = ManagerFactory.getEntityManager().find(ContentLocale.class, contentLocale.getId());
				contentLocale.setDirty(false);
				transaction.commit();
			}
		}
		finally
		{
			transaction.dispose();
		}
	}

	/**
	 * Removes version and data of a {@link Collection collection} of versions.
	 * 
	 * @param sessionConfig
	 *            the session config to use.
	 * @param sourceNode
	 *            the source node related to the content versions.
	 * @param config
	 *            the service instance config related to the content versions.
	 * @param toBeRemoved
	 *            the versions to be removed.
	 * @param transaction
	 *            the transaction to use.
	 * @since 6.0.0
	 */
	private void removeVersionAndData(SessionConfig sessionConfig, Node sourceNode, ServiceInstanceConfig config,
			Collection<ContentVersion> toBeRemoved, ITransaction transaction) throws PortalException
	{
		if (toBeRemoved != null && toBeRemoved.size() > 0)
		{
			for (ContentVersion version: toBeRemoved)
			{
				deleteData(sessionConfig, config, Collections.singletonList(version.getItemId()), sourceNode, transaction);
				version.delete();
			}
		}
	}

	/**
	 * Selects the versions to be removed.
	 * 
	 * @param contentLocale
	 *            the content locale.
	 * @param maxVersions
	 *            the maximum number of versions to keep.
	 * @return the versions to be removed.
	 * @since 6.0.0
	 */
	@SuppressWarnings("unchecked")
	private Collection<ContentVersion> selectVersions(ContentLocale contentLocale, int maxVersions)
	{
		return ManagerFactory.getEntityManager().createQuery(SELECT_VERSIONS_FOR_DELETION).setParameter(LOCALE_PARAM, contentLocale)
				.setParameter(MAX_VERSIONS_PARAM, maxVersions).setMaxResults(MAX_CONTENT_VERSIONS_BLOCK).getResultList();
	}

	private void deleteData(final SessionConfig sessionConfig, final ServiceInstanceConfig config, List<String> itemIdsTemp,
			final Node currentSourceDefinitionNode, final ITransaction transaction) throws PortalException
	{
		// if view do not delete data
		String viewName = XmlUtil.readNodeString("view", currentSourceDefinitionNode);
		if (viewName != null)
			return;

		final String primaryKeyFieldId = XmlUtil.readAttributeString("id", XmlUtil.selectSingleNode("fields/field[@isPrimaryKey='true']",
				currentSourceDefinitionNode));

		final TableSource source = (TableSource) SourceFactory.createSource(currentSourceDefinitionNode, new SourceContext(sessionConfig,
				config, transaction));
		deleteRelatedData(sessionConfig, config, itemIdsTemp, currentSourceDefinitionNode, primaryKeyFieldId, transaction);

		CollectionExecutor<String> executor = new CollectionExecutor<String>(itemIdsTemp);
		executor.execute(new CollectionExecutor.IIterationCommand<String>()
		{
			public void execute(List<String> subItems) throws PortalException
			{
				Parameters parameters = new Parameters();
				String[] arrItemIds = subItems.toArray(new String[subItems.size()]);
				parameters.setParameter(primaryKeyFieldId, arrItemIds);

				// delete from search index
				String indexerEngineId = source.getIndexerEngineId();
				String sourceContentFillerClassName = source.getSourceSearchContentFillerClassName();
				DouiIndexConfig douiIndexConfig = new DouiIndexConfig(indexerEngineId, sourceContentFillerClassName);
				IDouiIndexer douiIndexer = DouiIndexerFactory.getDouiIndexer();
				douiIndexer.removeSearchContents(new ParametersSourceDataWrapper(parameters), source, douiIndexConfig);

				// delete from persistence
				ContentQueryBuilder contentQueryBuilder = new ContentQueryBuilder(parameters, source, transaction);
				QueryDelete queryDelete = contentQueryBuilder.buildQueryDelete();
				DouiDaoFactory.getTableDao().executeDelete(queryDelete, transaction);
			}
		});
	}

	@SuppressWarnings("unchecked")
	private void deleteRelatedData(SessionConfig sessionConfig, ServiceInstanceConfig config, Collection<String> itemIdsTemp,
			Node currentSourceDefinitionNode, String foreignPrimaryKeyFieldId, final ITransaction transaction) throws PortalException
	{
		String sourceId = XmlUtil.readAttributeString("id", currentSourceDefinitionNode);

		deleteRelatedFiles(sessionConfig, config, itemIdsTemp, currentSourceDefinitionNode, foreignPrimaryKeyFieldId, transaction);
		;

		Node[] relationNodes = XmlUtil.selectNodes("source/relations/relation[@id='" + sourceId + "' or @foreignSourceId='" + sourceId
				+ "']", currentSourceDefinitionNode.getParentNode());

		for (Node relationNode : relationNodes)
		{
			final Node relationSourceNode = relationNode.getParentNode().getParentNode();

			if (XmlUtil.selectSingleNode("table", relationSourceNode) != null)
			{
				// get relationFieldNode
				boolean basedOnContentId = false;
				Node relationFieldNode = XmlUtil.selectSingleNode("relationField[@foreignFieldId='" + foreignPrimaryKeyFieldId + "']",
						relationNode);
				if (relationFieldNode == null)
				{
					basedOnContentId = true;
					String contentIdFieldId = XmlUtil.readAttributeString("id", XmlUtil.selectSingleNode(
							"fields/field[@isContentId='true']", currentSourceDefinitionNode));
					if (contentIdFieldId != null)
						relationFieldNode = XmlUtil.selectSingleNode("relationField[@foreignFieldId='" + contentIdFieldId + "']",
								relationNode);
					if (relationFieldNode == null)
						continue;
				}

				// get values by which the data to be deleted will be filtered
				final Collection<String> filterValues;
				if (basedOnContentId)
				{
					filterValues = new ArrayList<String>();
					CollectionExecutor<String> executor = new CollectionExecutor<String>(itemIdsTemp);
					executor.execute(new CollectionExecutor.IIterationCommand<String>()
					{
						public void execute(List<String> subItems)
						{
							// get contentIds of itemIdsTemp
							filterValues.addAll(ManagerFactory.getEntityManager().createQuery(
									"select distinct v.contentLocale.content.id from " + "ContentVersion v where v.itemId in (:itemIds)")
									.setParameter("itemIds", subItems).getResultList());
						}
					});
				}
				else
				{
					filterValues = itemIdsTemp;
				}

				// select all primary keys where the relation id in the itemIds
				// list
				final TableSource source = new TableSource(relationSourceNode, new SourceContext(sessionConfig, config, transaction));
				final String primaryKeyFieldId = XmlUtil.readAttributeString("id", XmlUtil.selectSingleNode(
						"fields/field[@isPrimaryKey='true']", relationSourceNode));
				final String relationFieldId = XmlUtil.readAttributeString("fieldId", relationFieldNode);

				final List<ISourceData> sourceDataRows = new ArrayList<ISourceData>();
				CollectionExecutor<String> executor = new CollectionExecutor<String>(filterValues);
				executor.execute(new CollectionExecutor.IIterationCommand<String>()
				{
					public void execute(List<String> subItems) throws PortalException
					{
						QueryBuilder queryBuilder = new QueryBuilder(null, source, transaction);

						QuerySelect querySelect = queryBuilder.buildQuerySelect();
						QuerySelect simpleQuerySelect = new QuerySelect();
						simpleQuerySelect.setTable(querySelect.getTable());
						QueryField primaryKeyField = querySelect.getFieldById(primaryKeyFieldId);
						simpleQuerySelect.addField(primaryKeyField);

						// filter by 'relationFieldId in (:filterValues)'
						QueryFilter queryFilter = new QueryFilter();
						queryFilter.setLeftField(querySelect.getFieldById(relationFieldId));
						queryFilter.setFilterOperator(IQueryFilter.FILTER_OPERATOR_IN);
						simpleQuerySelect.addFilter(queryFilter);

						String[] arrFilterValues = subItems.toArray(new String[subItems.size()]);
						queryFilter.setRightField(new QueryValue(arrFilterValues));

						TabularData tabularData = DouiDaoFactory.getTableDao().readData(simpleQuerySelect, transaction);
						sourceDataRows.addAll(tabularData.getRows());
					}
				});

				if (!sourceDataRows.isEmpty())
				{
					// copy itemIds from sourceDataRows into itemIdsToDelete
					List<String> itemIdsToDelete = new ArrayList<String>(sourceDataRows.size());
					for (ISourceData sourceData : sourceDataRows)
						itemIdsToDelete.add((String) sourceData.get(primaryKeyFieldId));

					// finally call delete on the relation table
					deleteData(sessionConfig, config, itemIdsToDelete, relationSourceNode, transaction);
				}
			}
		}
	}

	private void deleteRelatedFiles(final SessionConfig sessionConfig, final ServiceInstanceConfig config,
			Collection<String> itemIdsTemp, final Node currentSourceDefinitionNode, final String primaryKeyFieldId,
			final ITransaction transaction) throws PortalException
	{
		CollectionExecutor<String> executor = new CollectionExecutor<String>(itemIdsTemp);
		executor.execute(new CollectionExecutor.IIterationCommand<String>()
		{
			public void execute(List<String> subItems) throws PortalException
			{
				// remover datatype = "file"
				Node[] fileFieldNodes = XmlUtil.selectNodes("fields/field[@dataType='file']", currentSourceDefinitionNode);
				if (fileFieldNodes.length > 0 && !subItems.isEmpty())
				{
					String[] arrItemIds = new String[subItems.size()];
					arrItemIds = subItems.toArray(arrItemIds);

					Parameters parameters = new Parameters();
					parameters.setParameter(primaryKeyFieldId, arrItemIds);
					TableSource source = new TableSource(currentSourceDefinitionNode, new SourceContext(sessionConfig, config, transaction));
					QueryBuilder queryBuilder = new QueryBuilder(parameters, (TableSource) source, transaction);
					;
					QuerySelect querySelect = queryBuilder.buildQuerySelect();

					QuerySelect simpleQuerySelect = new QuerySelect();
					simpleQuerySelect.setTable(querySelect.getTable());

					for (Node fileFieldNode : fileFieldNodes)
					{
						String curFieldId = XmlUtil.readAttributeString("id", fileFieldNode);
						QueryField fileField = querySelect.getFieldById(curFieldId);
						simpleQuerySelect.addField(fileField);
					}

					QueryFilter queryFilter = new QueryFilter();
					queryFilter.setLeftField(querySelect.getFieldById(primaryKeyFieldId));
					queryFilter.setFilterOperator(IQueryFilter.FILTER_OPERATOR_IN);
					queryFilter.setRightField(new QueryValue(arrItemIds));

					simpleQuerySelect.addFilter(queryFilter);

					TabularData tabularData = DouiDaoFactory.getTableDao().readData(simpleQuerySelect, transaction);

					List<ISourceData> sourceDataRows = tabularData.getRows();

					for (ISourceData sourceDataRow : sourceDataRows)
					{
						for (Node fileFieldNode : fileFieldNodes)
						{
							String curFieldId = XmlUtil.readAttributeString("id", fileFieldNode);
							String fileId = (String) sourceDataRow.get(curFieldId);
							ManagerFactory.getFileManager().removeStoredFile(sessionConfig, fileId, transaction);
						}
					}
				}

				// remover dataType="files" (note: plural)
				Node[] filesFieldNodes = XmlUtil.selectNodes("fields/field[@dataType='files']", currentSourceDefinitionNode);
				if (filesFieldNodes.length > 0)
				{
					String[] arrItemIds = new String[subItems.size()];
					subItems.toArray(arrItemIds);

					Parameters parameters = new Parameters();
					parameters.setParameter(primaryKeyFieldId, arrItemIds);
					TableSource source = new TableSource(currentSourceDefinitionNode, new SourceContext(sessionConfig, config, transaction));
					QueryBuilder queryBuilder = new QueryBuilder(parameters, (TableSource) source, transaction);
					;
					QuerySelect querySelect = queryBuilder.buildQuerySelect();

					QuerySelect simpleQuerySelect = new QuerySelect();
					simpleQuerySelect.setTable(querySelect.getTable());

					for (Node filesFieldNode : filesFieldNodes)
					{
						String curFieldId = XmlUtil.readAttributeString("id", filesFieldNode);
						QueryField filesField = querySelect.getFieldById(curFieldId);
						simpleQuerySelect.addField(filesField);
					}

					QueryFilter queryFilter = new QueryFilter();
					queryFilter.setLeftField(querySelect.getFieldById(primaryKeyFieldId));
					queryFilter.setFilterOperator(IQueryFilter.FILTER_OPERATOR_IN);
					queryFilter.setRightField(new QueryValue(arrItemIds));
					simpleQuerySelect.addFilter(queryFilter);

					TabularData tabularData = DouiDaoFactory.getTableDao().readData(simpleQuerySelect, transaction);

					List<ISourceData> sourceDataRows = tabularData.getRows();

					for (ISourceData sourceDataRow : sourceDataRows)
					{
						for (Node filesFieldNode : filesFieldNodes)
						{
							String curFieldId = XmlUtil.readAttributeString("id", filesFieldNode);
							String filesId = (String) sourceDataRow.get(curFieldId);
							ManagerFactory.getFileManager().removeStoredFiles(sessionConfig, filesId, transaction);
						}
					}
				}
			}
		});
	}

	/**
	 * Returns whether this thread has been stopped or not.
	 * 
	 * @return whether this thread has been stopped or not.
	 * @since 6.0.0
	 */
	private boolean checkRunning() throws InterruptedException
	{
		if (Thread.interrupted())
			throw new InterruptedException();
		return maxEndTime - System.currentTimeMillis() > TIME_FOR_STOPPING;
	}
}
